package com.guit.apt;

import com.guit.annotations.Cache;
import com.guit.annotations.GuitService;
import com.guit.annotations.Ignore;
import com.sun.mirror.apt.AnnotationProcessor;
import com.sun.mirror.apt.AnnotationProcessorEnvironment;
import com.sun.mirror.apt.Filer;
import com.sun.mirror.declaration.AnnotationMirror;
import com.sun.mirror.declaration.AnnotationTypeDeclaration;
import com.sun.mirror.declaration.AnnotationTypeElementDeclaration;
import com.sun.mirror.declaration.AnnotationValue;
import com.sun.mirror.declaration.ClassDeclaration;
import com.sun.mirror.declaration.InterfaceDeclaration;
import com.sun.mirror.declaration.MethodDeclaration;
import com.sun.mirror.declaration.ParameterDeclaration;
import com.sun.mirror.declaration.TypeDeclaration;
import com.sun.mirror.type.ClassType;
import com.sun.mirror.type.DeclaredType;
import com.sun.mirror.type.TypeMirror;
import com.sun.mirror.type.VoidType;
import com.sun.mirror.util.DeclarationFilter;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map.Entry;

public class GuitServiceProcessor implements AnnotationProcessor {

  AnnotationProcessorEnvironment env;

  Filer filer;

  public GuitServiceProcessor(AnnotationProcessorEnvironment env) {
    this.env = env;
    filer = env.getFiler();
  }

  public AnnotationProcessorEnvironment getEnvironment() {
    return env;
  }

  @Override
  public void process() {
    for (InterfaceDeclaration d : DeclarationFilter.getFilter(InterfaceDeclaration.class).filter(
        env.getDeclarationsAnnotatedWith((AnnotationTypeDeclaration) env
            .getTypeDeclaration(GuitService.class.getCanonicalName())), InterfaceDeclaration.class)) {

      boolean requestFactory = false;

      Collection<AnnotationMirror> mirrors = d.getAnnotationMirrors();
      for (AnnotationMirror m : mirrors) {
        String name = m.getAnnotationType().getDeclaration().getQualifiedName();
        if (name.equals(GuitService.class.getCanonicalName())) {
          for (Entry<AnnotationTypeElementDeclaration, AnnotationValue> e : m.getElementValues()
              .entrySet()) {
            requestFactory = (Boolean) e.getValue().getValue();
          }
        }
      }

      if (!requestFactory) {
        generateRpcService(d);
      } else {
        // TODO
      }
    }
  }

  public void generateRpcService(InterfaceDeclaration d) {
    String dpackage = d.getPackage().getQualifiedName();
    if (!d.getPackage().getSimpleName().equals("server")) {
      env.getMessager().printError(d.getPosition(), "All services must be on a 'server' package");
      return;
    }

    String clientPkg = dpackage.substring(0, dpackage.lastIndexOf(".")) + ".client";
    String actionPkg = clientPkg + ".action";
    String hanlderPkg = dpackage + ".handler";

    Collection<? extends MethodDeclaration> methods = d.getMethods();

    ArrayList<MethodDeclaration> serviceMethods = new ArrayList<MethodDeclaration>();
    String serviceName = d.getSimpleName();
    for (MethodDeclaration m : methods) {
      if (m.getAnnotation(Ignore.class) == null) {
        serviceMethods.add(m);

        String methodName = m.getSimpleName();
        methodName = methodName.substring(0, 1).toUpperCase() + methodName.substring(1);
        String simpleName = methodName;
        String handlerClass = dpackage + ".handler." + methodName + "Handler";
        HashMap<String, String> actionFields = new HashMap<String, String>();

        StringBuilder gettersList = new StringBuilder();

        Collection<ParameterDeclaration> parameters = m.getParameters();
        for (ParameterDeclaration p : parameters) {
          String pName = p.getSimpleName();
          actionFields.put(pName, p.getType().toString());

          if (gettersList.length() > 0) {
            gettersList.append(", ");
          }
          pName = pName.substring(0, 1).toUpperCase() + pName.substring(1);
          gettersList.append("action.get" + pName + "()");
        }

        TypeMirror returnType = m.getReturnType();
        boolean doesntReturnsVoid = returnType != null && !(returnType instanceof VoidType);
        String responseFields = null;
        if (doesntReturnsVoid) {
          responseFields = returnType.toString();
        }
        generateAction(actionFields, responseFields, handlerClass, simpleName, actionPkg, m
            .getAnnotation(Cache.class) != null);

        PrintWriter writer = getPrintWriter(hanlderPkg + "." + simpleName + "Handler");

        String service = d.getQualifiedName();

        writer.println("package " + hanlderPkg + ";");
        writer.println();

        writer.println("import com.google.inject.Inject;");
        writer.println();
        writer.println("import com.guit.client.command.action.CommandException;");
        writer.println("import com.guit.client.command.action.Handler;");
        writer.println("import " + actionPkg + "." + simpleName + "Action;");
        writer.println("import " + actionPkg + "." + simpleName + "Response;");
        writer.println("import " + service + ";");
        Generated.printGeneratedImport(writer);
        writer.println();
        Generated.printGenerated(writer, serviceName);
        writer.println("public class " + simpleName + "Handler implements Handler<" + simpleName
            + "Action, " + simpleName + "Response> {");

        // Print all preprocesors injections
        ArrayList<String> preprocessors = new ArrayList<String>();
        Collection<AnnotationMirror> annotations = m.getAnnotationMirrors();
        for (AnnotationMirror mirror : annotations) {
          AnnotationTypeDeclaration declaration = mirror.getAnnotationType().getDeclaration();

          TypeDeclaration a = env.getTypeDeclaration(declaration.getQualifiedName());
          Collection<AnnotationMirror> aAnnotations = a.getAnnotationMirrors();
          for (AnnotationMirror aAnn : aAnnotations) {
            if (aAnn.getAnnotationType().getDeclaration().getQualifiedName().equals(
                "com.guit.server.command.PreprocesorAnnotation")) {
              for (Entry<AnnotationTypeElementDeclaration, AnnotationValue> entry : aAnn
                  .getElementValues().entrySet()) {
                try {
                  ClassDeclaration preprocesorType =
                      ((ClassType) entry.getValue().getValue()).getDeclaration();
                  writer.println();
                  writer.println("  @Inject");
                  String qualifiedName = preprocesorType.getQualifiedName();
                  String fieldName = qualifiedName.replaceAll("[.]", "_");
                  writer.println("  " + qualifiedName + " " + fieldName + ";");

                  String annotation =
                      mirror.getAnnotationType().getDeclaration().getQualifiedName();
                  String annFieldName = fieldName + "Annotation";
                  writer.append("  private static " + annotation + " " + annFieldName + ";");

                  writer.println("  static {");
                  writer.println("    try {");
                  writer.print("      " + annFieldName + " = " + service
                      + ".class.getDeclaredMethod(\"" + m.getSimpleName() + "\", new Class[]{");

                  // Method parameters
                  boolean first = true;
                  for (ParameterDeclaration p : m.getParameters()) {
                    if (!first) {
                      writer.print(",");
                    }

                    writer.print(((DeclaredType) p.getType()).getDeclaration().getQualifiedName()
                        + ".class");

                    first = false;
                  }

                  writer.println("}).getAnnotation(" + annotation + ".class);");

                  writer.println("    } catch(" + NoSuchMethodException.class.getCanonicalName()
                      + " e) {");
                  writer.println("      throw new " + RuntimeException.class.getCanonicalName()
                      + "(e);");
                  writer.println("    }");
                  writer.println("  }");

                  preprocessors.add(fieldName);
                } catch (ClassCastException e) {
                  continue;
                }
              }
            }
          }
        }

        writer.println();
        writer.println("  @Inject");
        writer.println("  " + serviceName + " service;");
        writer.println();
        writer.println("  @Override");
        writer.println("  public " + simpleName + "Response handle(" + simpleName
            + "Action action) throws CommandException {");

        // Preprocesors
        for (String p : preprocessors) {
          writer.println("    " + p + ".run(" + p + "Annotation);");
        }

        if (doesntReturnsVoid) {
          writer.println("    return new " + simpleName + "Response(service." + m.getSimpleName()
              + "(" + gettersList + "));");
        } else {
          writer.println("    service." + m.getSimpleName() + "(" + gettersList + ");");
          writer.println("    return new " + simpleName + "Response();");
        }

        writer.println("  }");
        writer.println("}");
        writer.close();
      }
    }

    // Generate service async
    String name = serviceName + "Async";
    PrintWriter writer = getPrintWriter(clientPkg + "." + name);
    writer.println("package " + clientPkg + ";");
    writer.println();
    writer.println("import com.google.inject.Inject;");
    writer.println();
    writer.println("import com.guit.client.async.AbstractAsyncCallback;");
    writer.println("import com.guit.client.command.Async;");
    writer.println("import com.guit.client.command.AsyncMethod;");
    writer.println("import com.guit.client.command.CommandService;");
    Generated.printGeneratedImport(writer);
    writer.println();
    writer.println("/**");
    writer.println(" * Async access to " + serviceName);
    writer.println(" */");
    Generated.printGenerated(writer, serviceName);
    writer.println("public class " + name + " {");
    writer.println();
    writer.println("  private final CommandService commandService;");
    writer.println();
    writer.println("  @Inject");
    writer.println("  public " + name + "(CommandService commandService) {");
    writer.println("    this.commandService = commandService;");
    writer.println("  }");
    writer.println();
    for (MethodDeclaration m : serviceMethods) {
      String methodName = m.getSimpleName();
      methodName = methodName.substring(0, 1).toUpperCase() + methodName.substring(1);
      String simpleName = methodName;

      String returnType = AptUtils.getTypeQualifiedName(m.getReturnType(), false);

      writer.println();
      writer.println("  /**");
      writer.println("   * Async call to " + serviceName + "." + methodName);
      writer.println("   */");
      writer.print("  public AsyncMethod<");
      writer.print(returnType);
      writer.print(">");
      writer.print(" ");
      writer.print(m.getSimpleName());
      writer.print("(");

      boolean first = true;
      for (ParameterDeclaration p : m.getParameters()) {
        if (!first) {
          writer.print(", ");
        } else {
          first = false;
        }
        writer.print(AptUtils.getTypeQualifiedName(p.getType(), false));
        writer.print(" ");
        writer.print(p.getSimpleName());
      }
      writer.println(") {");

      // Instantiate action and set values
      writer.println("    final " + actionPkg + "." + simpleName + "Action action = new "
          + actionPkg + "." + simpleName + "Action();");
      for (ParameterDeclaration p : m.getParameters()) {
        String parameterName = p.getSimpleName();
        writer.println("    action.set" + parameterName.substring(0, 1).toUpperCase()
            + parameterName.substring(1) + "(" + parameterName + ");");
      }

      // Async method
      writer.println("    return new AsyncMethod<" + returnType + ">() {");
      writer.println();
      writer.println("      public void fire(final Async<" + returnType + "> async) {");
      writer.println("        commandService.execute(action, new AbstractAsyncCallback<");
      writer.println("            " + actionPkg + "." + simpleName + "Response>() {");
      writer.println("          public void success(" + actionPkg + "." + simpleName
          + "Response response) {");
      writer.println("            async.success("
          + (returnType.equals(Void.class.getCanonicalName()) ? "null" : "response.getResult()")
          + ");");
      writer.println("          }");
      writer.println();
      writer.println("          public void failure(" + Throwable.class.getCanonicalName()
          + " ex) {");
      writer.println("            async.failure(ex);");
      writer.println("          }");
      writer.println("        });");
      writer.println("      }");
      writer.println("    };");
      writer.println("  }");
    }

    writer.println("}");
    writer.close();
  }

  public void generateAction(HashMap<String, String> actionFields, String responseField,
      String handlerClass, String simpleName, String packageName, boolean cacheable) {
    String actionName = simpleName + "Action";
    String responseName = simpleName + "Response";

    PrintWriter writer = getPrintWriter(packageName + "." + actionName);
    writer.println("package " + packageName + ";");

    if (cacheable) {
      writer.println("import com.guit.client.command.action.Cacheable;");
    }

    writer.println("import com.guit.client.command.HashCodeBuilder;");
    writer.println("import com.guit.client.command.action.Action;");
    writer.println("import com.guit.client.command.action.ActionHandler;");

    writer.println("import " + packageName + "." + responseName + ";");
    Generated.printGeneratedImport(writer);

    Generated.printGenerated(writer, simpleName);
    writer.println("@ActionHandler(\"" + handlerClass + "\")");
    writer.print("public class " + actionName + " implements Action<" + responseName + ">");

    // Cache
    if (cacheable) {
      writer.print(", Cacheable ");
    }

    writer.println(" {");

    StringBuilder actionString = new StringBuilder();

    for (Entry<String, String> f : actionFields.entrySet()) {
      String type = f.getValue();
      String name = f.getKey();

      printGetterAndSetter(writer, type, name, actionName);

      if (actionString.length() > 0) {
        actionString.append(", ");
      }
      actionString.append(name + "=" + "\" + " + name + " + \"");
    }

    writer.println("  public " + actionName + "() {");
    writer.println("  }");

    writer.println("  public boolean equals(Object o) {");
    writer.println("    if (this == null) {");
    writer.println("        return false;");
    writer.println("    }");
    writer.println("    if (this == o) {");
    writer.println("        return true;");
    writer.println("    }");
    writer.println("    if (o instanceof " + actionName
        + ") { return o.hashCode() == hashCode(); } else { return super.equals(o);}");
    writer.println("  }");

    writer.println("  public int hashCode() {");
    writer.print("    return new HashCodeBuilder()");
    for (Entry<String, String> f : actionFields.entrySet()) {
      String name = f.getKey();
      writer.print(".append(" + name + ")");
    }
    writer.println(".toHashCode();");
    writer.println("  }");

    writer.println("  public String toString() {");
    writer.println("      if (!com.google.gwt.core.client.GWT.isScript()) {");
    writer.println("          return \"" + actionName + "[" + actionString.toString() + "]\";");
    writer.println("      }");
    writer.println("      return super.toString();");
    writer.println("  }");

    writer.println("}");
    writer.close();

    writer = getPrintWriter(packageName + "." + responseName);
    writer.println("package " + packageName + ";");

    writer.println("import com.guit.client.command.action.Response;");
    Generated.printGeneratedImport(writer);

    Generated.printGenerated(writer, simpleName);
    writer.println("public class " + responseName + " implements Response {");

    StringBuilder responseString = new StringBuilder();
    if (responseField != null) {
      printGetterAndSetter(writer, responseField, "result", responseName);

      if (responseString.length() > 0) {
        responseString.append(", ");
      }
      responseString.append("result = \" + result + \"");
    }

    writer.println("  public String toString() {");
    writer.println("      if (!com.google.gwt.core.client.GWT.isScript()) {");
    writer.println("          return \"" + responseName + "[" + responseString.toString() + "]\";");
    writer.println("      }");
    writer.println("      return super.toString();");
    writer.println("  }");

    writer.println("  public " + responseName + "() {");
    writer.println("  }");

    if (responseField != null) {
      writer.println("  public " + responseName + "(" + responseField + " result) {");
      writer.println("    this.result = result;");
      writer.println("  }");
    }

    writer.println("}");
    writer.close();
  }

  private static void printGetterAndSetter(PrintWriter writer, String type, String name,
      String className) {
    writer.println("  private " + type + " " + name + ";");

    writer.println("  public " + type + " get" + name.substring(0, 1).toUpperCase()
        + name.substring(1) + "() {");
    writer.println("    return " + name + ";");
    writer.println("  }");

    writer.println("  public void set" + name.substring(0, 1).toUpperCase() + name.substring(1)
        + "(" + type + " " + name + ") {");
    writer.println("    this." + name + " = " + name + ";");
    writer.println("  }");
  }

  public PrintWriter getPrintWriter(String qualifiedName) {
    try {
      return filer.createSourceFile(qualifiedName);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }
}
